package com.fanvil.videoconference;

import android.annotation.SuppressLint;
import javax.microedition.khronos.egl.EGL10;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
//import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.view.Surface;

import com.fanvil.videoconference.FvlLogger;

import java.util.Objects;

@SuppressLint("NewApi")
public class FvlSurfaceManager {
    private static FvlLogger sLogger =
        FvlLogger.getLogger(FvlSurfaceManager.class.getSimpleName(), FvlLogger.DEBUG);

    private static final int EGL_RECORDABLE_ANDROID = 0x3142;

    private String mName = null;
    private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
    private EGLContext mEGLShareContext = EGL14.EGL_NO_CONTEXT;
    private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
    private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
    private EGLConfig[] mEGLConfigs = null;

    private Surface mSurface = null;
    private int mWidth = -1;
    private int mHeight = -1;

    /**
     * Creates an EGL context and an EGL surface.
     */
    /*public FvlSurfaceManager(String name, FvlSurfaceManager manager) {
        mName = name;
        if (manager != null) {
            mEGLShareContext = manager.getEGLContext();
            if (mEGLShareContext == null || mEGLShareContext == EGL14.EGL_NO_CONTEXT) {
                sLogger.e("[" + mName + "] Share context is null.");
            }
        } else {
            sLogger.e("[" + mName + "] Share manager is null.");
        }
    }
    */
    /**
     * Creates an EGL context and an EGL surface.
     */
    /*public FvlSurfaceManager(String name, Surface surface, FvlSurfaceManager manager) {
        mName = name;
        mSurface = surface;
        if (manager != null) {
            mEGLShareContext = manager.getEGLContext();
            if (mEGLShareContext == null || mEGLShareContext == EGL14.EGL_NO_CONTEXT) {
                sLogger.e("[" + mName + "] FvlSurfaceManager: Share context is null.");
            }
        } else {
            sLogger.e("[" + mName + "] FvlSurfaceManager: Share manager is null.");
        }
    }
    */
    /**
     * Creates an EGL context and an EGL surface.
     */
    public FvlSurfaceManager(Surface surface, FvlSurfaceManager manager) {
        mName = "FvlSurfaceManager";
        mSurface = surface;
        if (manager != null) {
            mEGLShareContext = manager.getEGLContext();
            if (mEGLShareContext == null || mEGLShareContext == EGL14.EGL_NO_CONTEXT) {
                sLogger.e("[" + mName + "] FvlSurfaceManager: Share context is null.");
            }
        } else {
            sLogger.e("[" + mName + "] FvlSurfaceManager: Share manager is null.");
        }
    }

    /**
     * Creates an EGL context and an EGL surface.
     */
    public FvlSurfaceManager(Surface surface) {
        mName = "FvlSurfaceManager";
        mSurface = surface;
    }

    /**
     * Creates an EGL context and an EGL surface.
     */
    /*public FvlSurfaceManager(String name, Surface surface) {
        mName = name;
        mSurface = surface;
    }
    */
    /**
     * Creates an EGL context and an EGL surface.
     */
    public FvlSurfaceManager(String name) {
        mName = name;
    }
/*
    public void updateSize(int width, int height) {
        if (sLogger.isLoggable()) sLogger.i("[" + mName + "] updateSize: current=" + mWidth + "x" + mHeight + " new=" +
                width + "x" + height);
        if (width != mWidth || height != mHeight) {
            if (sLogger.isLoggable()) sLogger.i("[" + mName + "] re-create EGLSurface");
            releaseEGLSurface();
            createEGLSurface();
            mWidth = getWidth();
            mHeight = getHeight();
        }
    }
*/

    public void setShareEGLContext(EGLContext shareContext) {
        mEGLShareContext = shareContext;
    }

    public EGLContext getEGLContext() {
        return mEGLContext;
    }

    public boolean createEGLSurface(Surface surface) {
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] createEGLSurface");

        /*
         * Check preconditions.
         */
        if (mEGLDisplay == null) {
            throw new RuntimeException("mEGLDisplay not initialized");
        }
        if (mEGLConfigs == null) {
            throw new RuntimeException("mEGLConfigs not initialized");
        }

        /*
         *  The window size has changed, so we need to create a new
         *  surface.
         */
        releaseEGLSurface();

        if (surface == null || !surface.isValid()) {
            sLogger.e("[" + mName + "] createEGLSurface: surface is invalid.");
            return false;
        }

        mSurface = surface;
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfigs[0], mSurface,
                surfaceAttribs, 0);
        if (mEGLSurface == null || mEGLSurface == EGL14.EGL_NO_SURFACE) {
            int error = EGL14.eglGetError();
            if (error == EGL14.EGL_BAD_NATIVE_WINDOW) {
                sLogger.e("[" + mName + "] createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
            }
            return false;
        }
        checkEglError("eglCreateWindowSurface");

        /*
         * Before we can issue GL commands, we need to make sure
         * the context is current and bound to a surface.
         */
        if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
            /*
             * Could not make the context current, probably because the underlying
             * SurfaceView surface has been destroyed.
             */
            return false;
        }

        EGLDisplay curDisplay = EGL14.eglGetCurrentDisplay();
        EGLContext curContext = EGL14.eglGetCurrentContext();
        EGLSurface curSurface = EGL14.eglGetCurrentSurface(EGL10.EGL_DRAW);
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] createEGLSurface: mEGLDisplay=" + mEGLDisplay + " mEGLContext=" +
                mEGLContext + " mEGLShareContext=" + mEGLShareContext + " mEGLSurface=" + mEGLSurface);
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] createEGLSurface: curDisplay=" +
                curDisplay + " curContext=" + curContext + " curSurface=" + curSurface);
        return true;
    }

    public void releaseEGLSurface() {
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] releaseEGLSurface");

        if (mEGLDisplay != null && mEGLDisplay != EGL14.EGL_NO_DISPLAY
                && mEGLSurface != null && mEGLSurface != EGL14.EGL_NO_SURFACE) {
            makeUnCurrent();
            EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
        }
        mEGLSurface = EGL14.EGL_NO_SURFACE;

        EGLDisplay curDisplay = EGL14.eglGetCurrentDisplay();
        EGLContext curContext = EGL14.eglGetCurrentContext();
        EGLSurface curSurface = EGL14.eglGetCurrentSurface(EGL10.EGL_DRAW);
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] releaseEGLSurface: mEGLDisplay=" + mEGLDisplay + " mEGLContext=" +
                mEGLContext + " mEGLShareContext=" + mEGLShareContext + " mEGLSurface=" + mEGLSurface);
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] releaseEGLSurface: curDisplay=" +
                curDisplay + " curContext=" + curContext + " curSurface=" + curSurface);
    }

    /**
     * Returns the Surface that the MediaCodec receives buffers from.
     */
    public Surface getSurface() {
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] getSurface " + mSurface);
        return mSurface;
    }

    /**
     * Queries the surface's width.
     */
    public int getWidth() {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY && mEGLSurface != EGL14.EGL_NO_SURFACE) {
            int[] value = new int[1];
            EGL14.eglQuerySurface(mEGLDisplay, mEGLSurface, EGL14.EGL_WIDTH, value, 0);
            if (sLogger.isLoggable()) sLogger.v("[" + mName + "] getWidth " + value[0]);
            return value[0];
        }
        return -1;
    }

    /**
     * Queries the surface's height.
     */
    public int getHeight() {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY && mEGLSurface != EGL14.EGL_NO_SURFACE) {
            int[] value = new int[1];
            EGL14.eglQuerySurface(mEGLDisplay, mEGLSurface, EGL14.EGL_HEIGHT, value, 0);
            if (sLogger.isLoggable()) sLogger.v("[" + mName + "] getHeight " + value[0]);
            return value[0];
        }
        return -1;
    }

    public void makeCurrent() {
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] makeCurrent: mEGLDisplay=" + mEGLDisplay + " mEGLContext=" +
                mEGLContext + " mEGLShareContext=" + mEGLShareContext + " mEGLSurface=" + mEGLSurface);
        EGLDisplay curDisplay = EGL14.eglGetCurrentDisplay();
        EGLContext curContext = EGL14.eglGetCurrentContext();
        EGLSurface curSurface = EGL14.eglGetCurrentSurface(EGL10.EGL_DRAW);
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] makeCurrent: before curDisplay=" +
                curDisplay + " curContext=" + curContext + " curSurface=" + curSurface);

        if (!mEGLContext.equals(EGL14.eglGetCurrentContext()) ||
                !mEGLSurface.equals(EGL14.eglGetCurrentSurface(EGL10.EGL_DRAW))) {
            if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
                throw new RuntimeException("eglMakeCurrent failed "
                        + EGL14.eglGetError());
            }
        }

        curDisplay = EGL14.eglGetCurrentDisplay();
        curContext = EGL14.eglGetCurrentContext();
        curSurface = EGL14.eglGetCurrentSurface(EGL10.EGL_DRAW);
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] makeCurrent: after curDisplay=" +
                curDisplay + " curContext=" + curContext + " curSurface=" + curSurface);
    }

    /**
     * Makes our EGL context and surface not current.
     */
    public void makeUnCurrent() {
        if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_CONTEXT)) {
            throw new RuntimeException("eglMakeCurrent failed");
        }
    }

    public int swapBuffer() {
        EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface);
        return EGL14.eglGetError();
    }

    /**
     * Sends the presentation time stamp to EGL.  Time is expressed in nanoseconds.
     */
    public void setPresentationTime(long nsecs) {
        //EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs);
        checkEglError("eglPresentationTimeANDROID");
    }

    /**
     * Prepares EGL.  We want a GLES 2.0 context and a surface that supports recording.
     * DON NOT call eglMakeCurrent
     */
    public void eglSetup() {
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] eglSetup");

        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("unable to get EGL14 display");
        }
        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
            throw new RuntimeException("unable to initialize EGL14");
        }

        // Configure EGL for recording and OpenGL ES 2.0.
        int[] attribList;
        if (mEGLShareContext == EGL14.EGL_NO_CONTEXT) {
            attribList = new int[] {
                    EGL14.EGL_RED_SIZE, 8,
                    EGL14.EGL_GREEN_SIZE, 8,
                    EGL14.EGL_BLUE_SIZE, 8,
                    EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                    EGL14.EGL_NONE
            };
        } else {
            attribList = new int[] {
                    EGL14.EGL_RED_SIZE, 8,
                    EGL14.EGL_GREEN_SIZE, 8,
                    EGL14.EGL_BLUE_SIZE, 8,
                    EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                    EGL_RECORDABLE_ANDROID, 1,
                    EGL14.EGL_NONE
            };
        }

        int[] numConfigs = new int[1];
        if (mEGLConfigs == null) {
            mEGLConfigs = new EGLConfig[1];
        }
        EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, mEGLConfigs, 0, mEGLConfigs.length,
                numConfigs, 0);
        checkEglError("eglCreateContext RGB888+recordable ES2");

        // Configure context for OpenGL ES 2.0.
        int[] attrib_list = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };

        if (mEGLShareContext == EGL14.EGL_NO_CONTEXT) {
            mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mEGLConfigs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0);
            if (sLogger.isLoggable()) sLogger.i("[" + mName + "] eglSetup: create egl context");
        } else {
            mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mEGLConfigs[0], mEGLShareContext, attrib_list, 0);
            if (sLogger.isLoggable()) sLogger.i("[" + mName + "] eglSetup: create egl share context");
        }
        checkEglError("eglCreateContext");

        // Create a window surface, and attach it to the Surface we received.
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        if (mSurface != null && mSurface.isValid()) {
            mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfigs[0], mSurface,
                    surfaceAttribs, 0);
            checkEglError("eglCreateWindowSurface");

            if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
                mWidth = getWidth();
                mHeight = getHeight();
            }
            if (sLogger.isLoggable()) sLogger.v("[" + mName + "] eglSetup: eglCreateWindowSurface EGLSurface width="
                    + mWidth + " height=" + mHeight);
        } else {
            mEGLSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfigs[0], surfaceAttribs, 0);
            checkEglError("eglCreatePbufferSurface");
            if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
                mWidth = getWidth();
                mHeight = getHeight();
            }
            if (sLogger.isLoggable()) sLogger.v("[" + mName + "] eglSetup: eglCreatePbufferSurface EGLSurface width="
                    + mWidth + " height=" + mHeight);
        }

        GLES20.glDisable(GLES20.GL_DEPTH_TEST);
        GLES20.glDisable(GLES20.GL_CULL_FACE);

        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] eglSetup: mEGLDisplay=" + mEGLDisplay + " mEGLContext=" +
                mEGLContext + " mEGLShareContext=" + mEGLShareContext + " mEGLSurface=" + mEGLSurface);
        EGLDisplay curDisplay = EGL14.eglGetCurrentDisplay();
        EGLContext curContext = EGL14.eglGetCurrentContext();
        EGLSurface curSurface = EGL14.eglGetCurrentSurface(EGL10.EGL_DRAW);
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] eglSetup: curDisplay=" +
                curDisplay + " curContext=" + curContext + " curSurface=" + curSurface);
    }

    /**
     * Discards all resources held by this class, notably the EGL context.  Also releases the
     * Surface that was passed to our constructor.
     */
    public void release() {
        if (sLogger.isLoggable()) sLogger.v("[" + mName + "] release");
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
            EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                    EGL14.EGL_NO_CONTEXT);
            if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
                EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
            }
            if (mEGLContext != EGL14.EGL_NO_CONTEXT) {
                EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
            }
            EGL14.eglReleaseThread();
            EGL14.eglTerminate(mEGLDisplay);
        }
        mEGLDisplay = EGL14.EGL_NO_DISPLAY;
        mEGLContext = EGL14.EGL_NO_CONTEXT;
        mEGLSurface = EGL14.EGL_NO_SURFACE;
        mEGLConfigs = null;
        mSurface = null;
    }

    /**
     * Checks for EGL errors. Throws an exception if one is found.
     */
    private void checkEglError(String msg) {
        int error;
        if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
            throw new RuntimeException(msg + "[" + mName + "] : EGL error: 0x" + Integer.toHexString(error));
        }
    }
}
